'''
@Project ：
@File    ：my12306.py
@IDE     ：PyCharm 
@Author  ：子协
@Date    ：2025-07-08 12:02 
'''
import requests
import re
import json
import time
from datetime import datetime
from enum import Enum

class ErrorCode(Enum):
    """错误码枚举类"""
    OK = 0                          # 正确
    ERR_LeftTicketURL = -1          # 错误_获取余票URL失败
    ERR_3CodeNotExit = -2           # 错误_车站对应的三字码不存在
    ERR_GetAllTrains = -3           # 错误_获取余票信息失败
    ERR_TrainNotExit = -4           # 错误_车次不存在
    ERR_NoTickets = -5              # 错误_没有余票
    ERR_TimeOut = -6                # 错误_超时

# 12306接口类
class Railway12306Client:
    def __init__(self):
        # 12306域名
        self.domain_12306 = r'https://kyfw.12306.cn'
        # 初始化url
        self.url_init = r'/otn/leftTicket/init'
        # 余票查询url 后边会根据初始化url的返回值进行对应修改
        self.url_ypcx = ''
        # 车站名三字码url
        self.url_stations_name = r"/otn/resources/js/framework/station_name.js"
        # 车站名三字码
        self.station_code = {}
        # 车次列表
        self.trains_list = []
        # 符合条件的车次列表
        self.filtered_trains_list = []
        # 有余票的车次信息
        self.available_tickets_list = []
        # 超时的车次信息
        self.timeout_trains_list = []
        # 座位类型对应的索引
        self.chairType_index = {"高级软卧": 0,
                               "软卧": 1,
                               "一等卧": 1,
                               "软座": 2,
                               "特等座": 3,
                               "无座": 4,
                               "硬卧": 5,
                               "二等卧": 5,
                               "硬座": 6,
                               "二等座": 7,
                               "一等座": 8,
                               "商务座": 9,
                               "动卧": 10}

    def Decode(self,string):
        """
        解码返回的数据
        Args:
            string:需要解码的字符串
        Returns:
            result:解码后的数据
        """
        _string = string.split('|')
        # 0:车次 1:出发时间 2:到达时间 3:高级软卧 4:软卧/一等卧 5:软座 6:特等座 7:无座 8:硬卧/二等卧 9:硬座 10:二等座 11:一等座 12:商务座 13:动卧
        result = (_string[3], _string[8], _string[9], _string[21], _string[23], _string[24], _string[25], _string[26],
                  _string[28], _string[29], _string[30], _string[31], _string[32], _string[33])
        return result

    def GetLeftTicketURL(self):
        """
        获取12306余票查询接口的URL(URL地址会随机改变)
        Args:

        Returns:
            ErrorCode.OK:获取URL成功
            ErrorCode.ERR_LeftTicketURL:获取URL失败
        """
        res = requests.get(self.domain_12306 + self.url_init)
        # 匹配可能包含空格和换行的模式
        pattern = r"var\s+CLeftTicketUrl\s*=\s*'(.*?)'\s*;"
        match = re.search(pattern, res.text, re.DOTALL)
        if match:
            url = match.group(1).strip()
            _url_ypcx = '/otn/' + url
            self.url_ypcx = _url_ypcx
            return ErrorCode.OK
        else:
            # print("错误:获取余票查询接口URL失败")
            return ErrorCode.ERR_LeftTicketURL


    def GetALLCode(self):
        """
        获取12306所有车站的三字码
        Args:

        Returns:

        """
        res = requests.get(self.domain_12306 + self.url_stations_name)
        stations_name = res.text
        stations_name = stations_name[20:]  # 去除掉前边的  var station_names ='
        stations_name = stations_name[:-2]  # 去除掉最后的  ':
        stations = stations_name.split('@')[1:]  # 以@划分并去掉第一个空元素
        for station in stations:
            station_msg = station.split('|')  # 以 | 划分
            name = station_msg[1]  # 第一个元素为 车站名
            code = station_msg[2]  # 第二个元素为 车站名对应的三字码
            self.station_code[name] = code

    def GetCodeByName(self,station_name):
        """
        根据车站名获取对应的三字码
        Args:
            station_name:需要获取三字码的车站名
        Returns:
            ErrorCode.ERR_3CodeNotExit: 车站对应的三字码不存在
            self.station_code[station_name]:车站对应的三字码
        """
        if station_name not in self.station_code:
            return ErrorCode.ERR_3CodeNotExit
        return self.station_code[station_name]

    def GetAllTrains(self,from_station_code, to_station_code, date):
        """
        获取日期{date}下所有的车次信息
        Args:
            from_station_code:起点车站三字码
            to_station_code:终点车站三字码
            date:日期
        Returns:
            ErrorCode.OK:获取成功
            ErrorCode.ERR_GetAllTrains: 获取失败
        """
        _trains_list = []
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
            "Connection": "keep-alive",
            "Content-Type": "application/json;charset=UTF-8",
            "Cookie": "CLIENT=zgzfw"
        }

        params = {"leftTicketDTO.train_date": date,
                  "leftTicketDTO.from_station": from_station_code,
                  "leftTicketDTO.to_station": to_station_code,
                  "purpose_codes": "ADULT"
                  }
        res = requests.get(self.domain_12306 + self.url_ypcx, headers=headers, params=params)
        # print(res.text)
        try:
            trains_json = json.loads(res.text)
            trains = trains_json["data"]["result"]

            for train_info in trains:
                decode_msg = self.Decode(train_info)
                # print("一等座:{},二等座:{},无座:{}".format(decode_msg[chairType_index["一等座"]+3],decode_msg[chairType_index["二等座"]+3],decode_msg[chairType_index["无座"]+3]))
                _trains_list.append(decode_msg)
            self.trains_list = _trains_list
            return ErrorCode.OK
        except:
            # print("错误:获取余票信息失败")
            return ErrorCode.ERR_GetAllTrains

    def GetTrainByTrainNumber(self,train_number_list):
        """
        获取指定车次信息
        Args:
            train_number_list:需要获取的车次号列表
        Returns:
            ErrorCode.ERR_TrainNotExit: 车次不存在
            ErrorCode.OK:获取成功
        """
        trains = []  # 符合条件的车次
        for train_info in self.trains_list:
            if train_info[0] in train_number_list:
                trains.append(train_info)
        if len(trains) == 0:
            return ErrorCode.ERR_TrainNotExit
        self.filtered_trains_list = trains
        return ErrorCode.OK

    def GetLeftTicketByChairType(self,chair_type_list):
        """
        获取指定车次指定座位类型的余票信息
        Args:
            chair_type_list:座位类型列表
        Returns:
            left_ticket_list: 余票信息列表
        """
        left_ticket_list = []  # 余票信息
        for train_info in self.filtered_trains_list:
            left_ticket = []
            for chair_type in chair_type_list:
                left_ticket.append(train_info[self.chairType_index[chair_type] + 3])
            left_ticket_list.append((train_info[0], train_info[1], left_ticket))  # (车次，出发时间，[座位对应的余票信息])
        return left_ticket_list

    def HasAvailableTickets(self,left_ticket_list):
        """
        判断是否有余票
        Args:
            left_ticket_list: 余票信息列表
        Returns:
            ErrorCode.OK: 有票
            ErrorCode.ERR_NoTickets:无票
        """
        for train_info in left_ticket_list:
            for left_ticket in train_info[2]:
                if left_ticket != '无' and left_ticket != '':
                    self.available_tickets_list = train_info
                    return ErrorCode.OK
        return ErrorCode.ERR_NoTickets

    def CheckDateReminder(self,left_ticket_list, mouth_day, hours_before):
        """
        判断是否到截至时间
        Args:
            left_ticket_list:余票列表
            mouth_day:车票日期(月-日)
            hours_before:截至小时数
        Returns:
            ErrorCode.ERR_TimeOut: 到达截至时间
            ErrorCode.OK:没有到达截至时间
        """
        current_time_s = time.time()  # 当前时间戳(S)
        hours_before_s = hours_before * 3600  # 截至时间(S)
        for train_info in left_ticket_list:
            departure_str = mouth_day + ' ' + train_info[1]
            departure_struct = time.strptime(departure_str, "%Y-%m-%d %H:%M")
            # print(departure_struct)
            departure_timestamp = time.mktime(departure_struct)  # 火车出发时间的时间戳(S)
            # print(departure_timestamp)
            if current_time_s >= (departure_timestamp - hours_before_s):  # 当前已经达到截至时间
                self.timeout_trains_list = train_info
                return ErrorCode.ERR_TimeOut    # 到达截止时间
        return ErrorCode.OK  # 都没有到达截至时间